// ========================================================================
// Copyright (c) 2009-2009 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
// The Eclipse Public License is available at 
// http://www.eclipse.org/legal/epl-v10.html
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
// You may elect to redistribute this code under either of these licenses. 
// ========================================================================

package org.eclipse.jetty.util.ajax;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.eclipse.jetty.util.ajax.JSON.Output;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

/**
 * Converts POJOs to JSON and vice versa. The key difference: - returns the actual object from Convertor.fromJSON (JSONObjectConverter returns a Map) - the getters/setters are resolved at initialization (JSONObjectConverter resolves it at runtime) - correctly sets the number fields
 */
@SuppressWarnings({ "unchecked", "rawtypes" })
public class JSONPojoConvertor implements JSON.Convertor
{

	private static final Logger LOG = Log.getLogger(JSONPojoConvertor.class);
	public static final Object[] GETTER_ARG = new Object[] {}, NULL_ARG = new Object[] { null };
	private static final Map<Class<?>, NumberType> __numberTypes = new HashMap<Class<?>, NumberType>();

	public static NumberType getNumberType(Class<?> clazz)
	{
		return __numberTypes.get(clazz);
	}

	protected boolean _fromJSON;
	protected Class<?> _pojoClass;
	protected Map<String, Method> _getters = new HashMap<String, Method>();
	protected Map<String, Setter> _setters = new HashMap<String, Setter>();
	protected Set<String> _excluded;

	/**
	 * @param pojoClass The class to convert
	 */
	public JSONPojoConvertor(Class<?> pojoClass)
	{
		this(pojoClass, (Set<String>)null, true);
	}

	/**
	 * @param pojoClass The class to convert
	 * @param excluded The fields to exclude
	 */
	public JSONPojoConvertor(Class<?> pojoClass, String[] excluded)
	{
		this(pojoClass, new HashSet<String>(Arrays.asList(excluded)), true);
	}

	/**
	 * @param pojoClass The class to convert
	 * @param excluded The fields to exclude
	 */
	public JSONPojoConvertor(Class<?> pojoClass, Set<String> excluded)
	{
		this(pojoClass, excluded, true);
	}

	/**
	 * @param pojoClass The class to convert
	 * @param excluded The fields to exclude
	 * @param fromJSON If true, add a class field to the JSON
	 */
	public JSONPojoConvertor(Class<?> pojoClass, Set<String> excluded, boolean fromJSON)
	{
		_pojoClass = pojoClass;
		_excluded = excluded;
		_fromJSON = fromJSON;
		init();
	}

	/**
	 * @param pojoClass The class to convert
	 * @param fromJSON If true, add a class field to the JSON
	 */
	public JSONPojoConvertor(Class<?> pojoClass, boolean fromJSON)
	{
		this(pojoClass, (Set<String>)null, fromJSON);
	}

	/* ------------------------------------------------------------ */
	protected void init()
	{
		Method[] methods = _pojoClass.getMethods();
		for (int i = 0; i < methods.length; i++)
		{
			Method m = methods[i];
			if (!Modifier.isStatic(m.getModifiers()) && m.getDeclaringClass() != Object.class)
			{
				String name = m.getName();
				switch (m.getParameterTypes().length)
				{
					case 0:

						if (m.getReturnType() != null)
						{
							if (name.startsWith("is") && name.length() > 2)
								name = name.substring(2, 3).toLowerCase() + name.substring(3);
							else if (name.startsWith("get") && name.length() > 3)
								name = name.substring(3, 4).toLowerCase() + name.substring(4);
							else
								break;
							if (includeField(name, m))
								addGetter(name, m);
						}
						break;
					case 1:
						if (name.startsWith("set") && name.length() > 3)
						{
							name = name.substring(3, 4).toLowerCase() + name.substring(4);
							if (includeField(name, m))
								addSetter(name, m);
						}
						break;
				}
			}
		}
	}

	/* ------------------------------------------------------------ */
	protected void addGetter(String name, Method method)
	{
		_getters.put(name, method);
	}

	/* ------------------------------------------------------------ */
	protected void addSetter(String name, Method method)
	{
		_setters.put(name, new Setter(name, method));
	}

	/* ------------------------------------------------------------ */
	protected Setter getSetter(String name)
	{
		return _setters.get(name);
	}

	/* ------------------------------------------------------------ */
	protected boolean includeField(String name, Method m)
	{
		return _excluded == null || !_excluded.contains(name);
	}

	/* ------------------------------------------------------------ */
	protected int getExcludedCount()
	{
		return _excluded == null ? 0 : _excluded.size();
	}

	/* ------------------------------------------------------------ */
	public Object fromJSON(Map object)
	{
		Object obj = null;
		try
		{
			obj = _pojoClass.newInstance();
		} catch (Exception e)
		{
			// TODO return Map instead?
			throw new RuntimeException(e);
		}

		setProps(obj, object);
		return obj;
	}

	/* ------------------------------------------------------------ */
	public int setProps(Object obj, Map<?, ?> props)
	{
		int count = 0;
		for (Iterator<?> iterator = props.entrySet().iterator(); iterator.hasNext();)
		{
			Map.Entry<?, ?> entry = (Map.Entry<?, ?>)iterator.next();
			Setter setter = getSetter((String)entry.getKey());
			if (setter != null)
			{
				try
				{
					setter.invoke(obj, entry.getValue());
					count++;
				} catch (Exception e)
				{
					// TODO throw exception?
					LOG.warn(_pojoClass.getName() + "#" + setter.getPropertyName() + " not set from " +
						(entry.getValue().getClass().getName()) + "=" + entry.getValue().toString());
					log(e);
				}
			}
		}
		return count;
	}

	/* ------------------------------------------------------------ */
	public void toJSON(Object obj, Output out)
	{
		if (_fromJSON)
			out.addClass(_pojoClass);
		for (Map.Entry<String, Method> entry: _getters.entrySet())
		{
			try
			{
				out.add(entry.getKey(), entry.getValue().invoke(obj, GETTER_ARG));
			} catch (Exception e)
			{
				// TODO throw exception?
				LOG.warn("{} property '{}' excluded. (errors)", _pojoClass.getName(),
					entry.getKey());
				log(e);
			}
		}
	}

	/* ------------------------------------------------------------ */
	protected void log(Throwable t)
	{
		LOG.ignore(t);
	}

	/* ------------------------------------------------------------ */
	public static class Setter
	{

		protected String _propertyName;
		protected Method _setter;
		protected NumberType _numberType;
		protected Class<?> _type;
		protected Class<?> _componentType;

		public Setter(String propertyName, Method method)
		{
			_propertyName = propertyName;
			_setter = method;
			_type = method.getParameterTypes()[0];
			_numberType = __numberTypes.get(_type);
			if (_numberType == null && _type.isArray())
			{
				_componentType = _type.getComponentType();
				_numberType = __numberTypes.get(_componentType);
			}
		}

		public String getPropertyName()
		{
			return _propertyName;
		}

		public Method getMethod()
		{
			return _setter;
		}

		public NumberType getNumberType()
		{
			return _numberType;
		}

		public Class<?> getType()
		{
			return _type;
		}

		public Class<?> getComponentType()
		{
			return _componentType;
		}

		public boolean isPropertyNumber()
		{
			return _numberType != null;
		}

		public void invoke(Object obj, Object value) throws IllegalArgumentException,
			IllegalAccessException, InvocationTargetException
		{
			if (value == null)
				_setter.invoke(obj, NULL_ARG);
			else
				invokeObject(obj, value);
		}

		protected void invokeObject(Object obj, Object value) throws IllegalArgumentException,
			IllegalAccessException, InvocationTargetException
		{

			if (_type.isEnum())
			{
				if (value instanceof Enum)
					_setter.invoke(obj, new Object[] { value });
				else
					_setter.invoke(obj, new Object[] { Enum.valueOf((Class<? extends Enum>)_type, value.toString()) });
			}
			else if (_numberType != null && value instanceof Number)
			{
				_setter.invoke(obj, new Object[] { _numberType.getActualValue((Number)value) });
			}
			else if (Character.TYPE.equals(_type) || Character.class.equals(_type))
			{
				_setter.invoke(obj, new Object[] { String.valueOf(value).charAt(0) });
			}
			else if (_componentType != null && value.getClass().isArray())
			{
				if (_numberType == null)
				{
					int len = Array.getLength(value);
					Object array = Array.newInstance(_componentType, len);
					try
					{
						System.arraycopy(value, 0, array, 0, len);
					} catch (Exception e)
					{
						// unusual array with multiple types
						LOG.ignore(e);
						_setter.invoke(obj, new Object[] { value });
						return;
					}
					_setter.invoke(obj, new Object[] { array });
				}
				else
				{
					Object[] old = (Object[])value;
					Object array = Array.newInstance(_componentType, old.length);
					try
					{
						for (int i = 0; i < old.length; i++)
							Array.set(array, i, _numberType.getActualValue((Number)old[i]));
					} catch (Exception e)
					{
						// unusual array with multiple types
						LOG.ignore(e);
						_setter.invoke(obj, new Object[] { value });
						return;
					}
					_setter.invoke(obj, new Object[] { array });
				}
			}
			else
				_setter.invoke(obj, new Object[] { value });
		}
	}

	public interface NumberType
	{

		public Object getActualValue(Number number);
	}

	public static final NumberType SHORT = new NumberType()
	{

		public Object getActualValue(Number number)
		{
			return new Short(number.shortValue());
		}
	};

	public static final NumberType INTEGER = new NumberType()
	{

		public Object getActualValue(Number number)
		{
			return new Integer(number.intValue());
		}
	};

	public static final NumberType FLOAT = new NumberType()
	{

		public Object getActualValue(Number number)
		{
			return new Float(number.floatValue());
		}
	};

	public static final NumberType LONG = new NumberType()
	{

		public Object getActualValue(Number number)
		{
			return number instanceof Long ? number : new Long(number.longValue());
		}
	};

	public static final NumberType DOUBLE = new NumberType()
	{

		public Object getActualValue(Number number)
		{
			return number instanceof Double ? number : new Double(number.doubleValue());
		}
	};

	static
	{
		__numberTypes.put(Short.class, SHORT);
		__numberTypes.put(Short.TYPE, SHORT);
		__numberTypes.put(Integer.class, INTEGER);
		__numberTypes.put(Integer.TYPE, INTEGER);
		__numberTypes.put(Long.class, LONG);
		__numberTypes.put(Long.TYPE, LONG);
		__numberTypes.put(Float.class, FLOAT);
		__numberTypes.put(Float.TYPE, FLOAT);
		__numberTypes.put(Double.class, DOUBLE);
		__numberTypes.put(Double.TYPE, DOUBLE);
	}
}
